/*
 * Copyright (c) 2009
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.dcarew.hudson;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.logging.impl.NoOpLog;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpVersion;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.SingleClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.dcarew.hudson.utils.HudsonUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * 
 * 
 * @author Devon Carew
 */
public class HudsonCommunications
{
	private String		baseURL;
	private boolean 	allowSelfSigned;
	private String		username;
	private String		password;
	private String		view;
	
	private Exception 	exception;
	private boolean 	hasFailedprojects;
	private boolean 	hasRunningProjects;
	private String		summary;
	private String 		description;
	private List		views = new ArrayList();
	private List		failed;
	
	
	public HudsonCommunications()
	{
		
	}
	
	static
	{
		// Adjust httpclient's logging preferences - less verbose.
		// [Fatal Error] :1:50: White spaces are required between publicId and systemId.
		
		// this doesn't seem to work -
		System.setProperty("org.apache.commons.logging.Log", NoOpLog.class.getName());
	}
	
	public void initFromPreferences()
	{
		setBaseURL(HudsonPlugin.getBaseURL());
		setAllowSelfSigned(HudsonPlugin.getAllowSelfSigned());
		
		HudsonCredentials credentials = HudsonPlugin.getUserCredentials();
		
		if (credentials != null)
		{
			setUsername(credentials.getUserName());
			setPassword(credentials.getPassword());
		}
		
		setView(HudsonPlugin.getHudsonView());
	}
	
	public String getBaseURL()
	{
		return baseURL;
	}
	
	public void setBaseURL(String url)
	{
		baseURL = url;
	}
	
	public void setAllowSelfSigned(boolean value)
	{
		allowSelfSigned = value;
	}
	
	public void setUsername(String username)
	{
		this.username = username;
	}
	
	public void setPassword(String password)
	{
		this.password = password;
	}
	
	public void setView(String view)
	{
		this.view = view;
	}
	
	public String getView()
	{
		return view;
	}
	
	public List getViews()
	{
		return views;
	}
	
	public List getFailedProjects()
	{
		return failed;
	}
	
	public boolean updateStatus()
	{
		String fileContents = null;
		
		if (baseURL == null || baseURL.length() == 0)
		{
			summary = "";
			//summary = "No Hudson server specified";
			
			return false;
		}
		
		try
		{
			InputStream in = connect(baseURL + HudsonPlugin.HUDSON_XML_API);
			
			byte[] data = readBytes(in);
			
			fileContents = new String(data);
			
			if (isHtml(fileContents))
				throw new IOException("communications error");
			
			parseResults(new ByteArrayInputStream(data), false);
			
			return true;
		}
		catch (Exception e)
		{
			exception = e;
			
			String className = e.getClass().getName();
			
			if (className.indexOf('.') != -1)
				className = className.substring(className.lastIndexOf('.') + 1);
			
			if (e.getMessage() != null)
				summary = className + ": " + e.getMessage();
			else
				summary = className;
			
			if (isHtml(fileContents))
			{
				initHtmlError(fileContents);
			}
			else
			{
				description = summary;
			}
			
			summary = className;
			
			return false;
		}
	}
	
	private byte[] readBytes(InputStream in)
		throws IOException
	{
		ByteArrayOutputStream 	out = new ByteArrayOutputStream();
		byte[] 					buffer = new byte[4096];
		
		int 					count = in.read(buffer);
		
		while (count != -1)
		{
			out.write(buffer, 0, count);
			count = in.read(buffer);
		}
		
		return out.toByteArray();
	}

	public Exception getException()
	{
		return exception;
	}
	
	public boolean getHasFailedprojects()
	{
		return hasFailedprojects;
	}
	
	public boolean getHasRunningProjects()
	{
		return hasRunningProjects;
	}
	
	public String getSummary()
	{
		return summary;
	}
	
	public String getDescription()
	{
		return description;
	}
	
	private InputStream connect(String url)
		throws IOException, NoSuchAlgorithmException, KeyManagementException
	{
		TrustManager easyTrustManager = new X509TrustManager() {
		    public void checkClientTrusted(X509Certificate[] chain, String authType)
		    	throws CertificateException
		    {
		    	
		    }
		    
		    public void checkServerTrusted(X509Certificate[] chain, String authType)
		    	throws CertificateException
		    {
		    	
		    }
		    
		    public X509Certificate[] getAcceptedIssuers()
		    {
		        return null;
		    }
		};
		
		HttpParams params = new BasicHttpParams();
		HttpConnectionParams.setConnectionTimeout(params, 20 * 1000);
		HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
		
		SSLContext sslcontext = SSLContext.getInstance("TLS");
		
		if (allowSelfSigned)
		{
			sslcontext.init(null, new TrustManager[] { easyTrustManager }, null);
		}
		else
		{
			sslcontext.init(null, null, null);
		}
		
		SSLSocketFactory sf = new SSLSocketFactory(sslcontext);
		
		Scheme httpScheme = new Scheme("http", PlainSocketFactory.getSocketFactory(), 80);
		
		SSLSocketFactory socketFactory = SSLSocketFactory.getSocketFactory();
		
		socketFactory.setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER);
		
		Scheme httpsScheme = new Scheme("https", sf, 443);
		
		SchemeRegistry schemeRegistry = new SchemeRegistry();
		
		schemeRegistry.register(httpScheme);
		schemeRegistry.register(httpsScheme);
		
		ClientConnectionManager cm = new SingleClientConnManager(params, schemeRegistry);
		
		HttpClient 		httpClient = new DefaultHttpClient(cm, params);
		
		HttpContext 	localContext = new BasicHttpContext();
		
		if (username != null && username.length() > 0)
		{
			UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(username, password);
			
			CredentialsProvider credsProvider = new BasicCredentialsProvider();
			credsProvider.setCredentials(new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT), credentials);
			
			localContext.setAttribute(ClientContext.CREDS_PROVIDER, credsProvider);
		}
		
		HttpGet 		httpGet = new HttpGet(url);
		HttpResponse 	response = httpClient.execute(httpGet, localContext);
		HttpEntity 		entity = response.getEntity();
		
		if (entity != null)
		{
		    return entity.getContent();
		}
		else
		{
			return null;
		}
	}	
	
	private void parseResults(InputStream in, boolean inView)
		throws SAXException, IOException, ParserConfigurationException, FactoryConfigurationError, KeyManagementException, NoSuchAlgorithmException
	{
		// <hudson>
		//   <job>
		//     <name>Analysis</name>
		//     <color>dsfsdf</color>  (one of blue, blue_anime, red, red_?)
		
		DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
		
		Document doc = builder.parse(in);
		
		//String desc = getSubNodeValue(doc.getElementsByTagName(inView ? "listView" : "hudson"), "description");
		//String desc = getSubNodeValue(doc.getElementsByTagName("hudson"), "description");
		
		NodeList projects = doc.getElementsByTagName("job");
		
		boolean allOK = true;
		
		String runningProjects = "";
		String failedProjects = "";
		
		failed = new ArrayList();
		
		int projectCount = 0;
		
		for (int i = 0; i < projects.getLength(); i++)
		{
			Node projectNode = projects.item(i);
			
			String projectName = getSubNodeValue(projectNode, "name");
			String color = getSubNodeValue(projectNode, "color");
			
			if (projectName != null && color != null)
			{
				// blue, red, disabled, blue_anime, ...
				if (color.indexOf("_anime") != -1)
				{
					if (runningProjects.length() > 0)
						runningProjects += ", " + projectName;
					else
						runningProjects = projectName;
					
					hasRunningProjects = true;
				}
				
				if (color.startsWith("red"))
				{
					allOK = false;
					
					if (failedProjects.length() > 0)
						failedProjects += ", " + projectName;
					else
						failedProjects = projectName;
					
					failed.add(projectName);
				}
				
				if (!"disabled".equals(color))
				{
					projectCount++;
				}
			}
		}
		
		summary = HudsonUtils.pluralize(projectCount, "project");
		
		description = "";
		
		if (runningProjects.length() > 0)
			description = "Building: " + runningProjects + "\n";
		
		if (failedProjects.length() > 0)
			description += "Failed: " + failedProjects + "\n";
		
		description = description.trim();
		
		if (description.length() == 0)
		{
			description = getSaying();
		}
		
		hasFailedprojects = !allOK;
		
		// <primaryView>
		// 
		// <view>
		//     <name>All</name> 
		//     <url></url> 
		// </view>
		
		if (!inView)
		{
			String primaryView = null;
			
			primaryView = getSubNodeValue(doc.getElementsByTagName("primaryView"), "name");
			
			NodeList nodeList = doc.getElementsByTagName("view");
			Map viewMap = new HashMap();
			
			for (int i = 0; i < nodeList.getLength(); i++)
			{
				Node node = nodeList.item(i);
				
				String viewName = getSubNodeValue(node, "name");
				
				views.add(viewName);
				
				viewMap.put(viewName, getSubNodeValue(node, "url"));
			}
			
			if (view == null)
			{
				view = primaryView;
			}
			else
			{
				if (!views.contains(view))
					view = primaryView;
			}
			
			if (view != null && !view.equals(primaryView))
			{
				// We need to reparse -
				String url = (String)viewMap.get(view);
				
				InputStream inputStream = connect(url + HudsonPlugin.HUDSON_XML_API);
				
				byte[] data = readBytes(inputStream);
				
				String fileContents = new String(data);
				
				if (isHtml(fileContents))
					throw new IOException("communications error");
				
				parseResults(new ByteArrayInputStream(data), true);
			}
		}
	}
	
	private String getSubNodeValue(NodeList nodeList, String name)
	{
		if (nodeList != null && nodeList.getLength() > 0)
			return getSubNodeValue(nodeList.item(0), name);
		
		return null;
	}

	private String getSubNodeValue(Node node, String name)
	{
		if (node == null)
			return null;
		
		NodeList children = node.getChildNodes();
		
		for (int index = 0; index < children.getLength(); index++)
		{
			Node child = children.item(index);
			
			if (name.equals(child.getNodeName()))
				return getText(child);
		}
		
		return null;
	}
	
	private static Random random = new Random();
	
	private String getSaying()
	{
		return zens[random.nextInt(zens.length)];
	}
	
	private String getText(Node child)
	{
		if (child.getFirstChild() != null)
			return child.getFirstChild().getNodeValue();
		
		return null;
	}
	
	private boolean isHtml(String fileContents)
	{
/*
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>401 Authorization Required</title>
</head><body>
<h1>Authorization Required</h1>
<p>This server could not verify that you
are authorized to access the document
requested.  Either you supplied the wrong
credentials (e.g., bad password), or your
browser doesn't understand how to supply
the credentials required.</p>
<hr>
</body></html>
 */		
		
		if (fileContents == null)
			return false;
		
		return fileContents.toLowerCase().indexOf("<html>") != -1;
	}

	private void initHtmlError(String fileContents)
	{
		String str = getSubString(fileContents, "<title>", "</title>");
		
		if (str != null)
			summary = "Error: " + str;
		
		description = fileContents;
		
		int index = fileContents.toLowerCase().indexOf("<html>");
		
		if (index != -1)
			description = fileContents.substring(index);
		
		description = summary + "\n" + description;
	}
	
	private String getSubString(String fileContents, String startStr, String endStr)
	{
		String temp = fileContents.toLowerCase();
		
		int start = temp.indexOf(startStr);
		int end = temp.indexOf(endStr);
		
		if (start == -1 || end == -1)
			return null;
		
		return fileContents.substring(start + startStr.length(), end).trim();
	}
	
	private static String[] zens;
	
	static
	{
		zens = new String[] {
			"He who thinks he know everything, knows nothing",
			"When there is nothing more important to you than your freedom, you can get out of any restraints",
			"He who chases two rabbits, goes home hungry",
			"Within darkness there is light, within light there is darkness, thus within fear there is strength",
			"A journey of a thousand miles begins with a single step",
			"Seek not to follow in the footsteps of the wise, seek what they sought",
			"Perfection is a direction, not a state of being",
			"If the doors of perception were cleaned everthing would appear to man as it is, infinite",
			"If you cannot find the truth right where you are, where else do you expect to find it?",
			"The only zen you find on the tops of mountians is the zen you bring up there",
			"We spar around the ring and suppose, but the secret sits in the middle and knows",
			"Things are entirely what they appear to be, and behind them ... there is nothing",
			"Our life is often frittered away by detail ... simplify, simplify",
			"It is good to have an end to journey toward; but it is the journey that matters, in the end",
			"If a man wishes to be sure of the road he treads on, he must close his eyes and walk in the dark",
			"One cannot step twice into the same river",
			"Every exit is an entry somewhere else",
			"The Way is not difficult, only there must be no wanting or not wanting",
			"How shall I grasp it? Do not grasp it, that which remains where there is no more grasping is the self",
			"People who study zen and don't see it, it is because they approach too eagerly",
			"We think in generalities, but we live in detail",
			"When the student is ready, the Master appears",
			"Teachers open the door, but you must enter by yourself",
			"In this very breath that we take now lies the secret that all great teachers try to tell us",
			"The true marksman aims at himself",
			"When hungry, eat; when tired, close your eyes. Fools may laugh at me, but wise men will know what I mean",
			"The ones who crave the fewest things, are nearest to their goals",
			"One see great things from the valley, only small things from the peak",
			"It is only with the heart that one can see; that what is invisible to the eye",
			"We shape clay into a pot, but it is the emptiness inside that holds whatever we want",
			"No one's mouth is big enough to utter the whole thing",
			"Anything more than the truth would be too much",
			"Men argue, nature acts",
			"A heavy snowfall disappears into the sea, what silence!",
			"The higher the moneky climbs, the more it sees, hence it keeps climbing",
			"It is as hard to see one's self as to look backwards without turning around",
			"If you gaze for long into darkness, the darkness also gazes into you",
			"He who knows others is wise. He who knows himself is enlightened",
			"Out of clutter, find simplicity. From discord, find harmony. In the middle of difficulty lies opportunity",
			"Be the master of your mind rather then mastered by it",
			"There is nothing either good or bad but thinking makes it so",
			"You can only find truth with logic, if you have already found truth without it",
			"When an ordinary man attains knowledge he is a sage; when a sage attains understanding, he is an ordinary man",
			"Talking about zen all the time is like looking for fish tracks in a dry riverbed",
			"When you can do nothing, what can you do?",
			"Wisdom is like a clear pond - it can be entered from any side",
			"The more you know the less you understand",
			"Meditation is not a means to an end. It is both the means and the end",
			"Both speech and silence transgress",
			"What is the sound of one hand clapping?",
			"With time and patience, the mulberry leaf becomes silk",
			"You cannot shake hands with a clenched fist",
			"Judge me by what I do in moments of conflict and challenge not comfort",
			"There are three hard things: steel, a diamond and to know ones's self",
			"Victory lies in overcoming obstacles every day",
			"Pick your battles, then act with courage",
			"Become all that you are able of becoming",
			"Enjoy every minute!",
			"Small opportunities are often the beginning of great achievments",
			"Some people dream of success - while others wake up and work hard at it",
			"Communication is the secret to success - pass it on!",
			"If you are not riding the wave of change - you'll find yourself beneath it",
			"Always use the word imposssible with the greatest caution",
			"We become successful by helping others become successful",
			"It is better to fail with honor than to win by deceit",
			"The time is always now",
			"If you refuse to accept anything but the very best, you often get it",
			"It only takes a single idea, a single action to move the world",
			"A bad word whispered will echo a hundred miles",
			"A bad workman blames his tools",
			"A closed mind is like a closed book, just a block of wood",
			"A crisis is an opportunity riding the dangerous wind",
			"A good neighbor is a found treasure",
			"A needle is not sharp at both ends",
			"A person who says it can't be done shouldn't interupt the man doing it",
			"A rumour goes in one ear and out many mouths",
			"A single beam cannot support a great house",
			"A smile will gain you ten more years of life",
			"A wise man makes his own decisions, an ignorant follows public opinion",
			"All things at first appear difficult",
			"Better do it than wish it done",
			"Count not what is lost but what is left",
			"Deal with the faults of others as gently as with your own",
			"Different flowers look good to different people",
			"Distant water won't help put out a fire close at hand",
			"Do not have each foot on a different boat",
			"Don't count your chickens before they hatch",
			"Don't cross the bridge until you come to it",
			"Failing to plan is planning to fail",
			"Giving your son a skill is better then giving him one thousand gold pieces",
			"Great souls have wills, feeble ones only wishes",
			"Hatred corrodes the vessel in which it is stored",
			"If you always give, you will always have",
			"If you don't scale the mountain you can't view the plane",
			"If you get up one more time then you fail you will make it though",
			"It is easier to know how to do, than it is to do",
			"Laws control the lesser man right conduct controls the greater one",
			"Learning is a weightless treasure you can always carry easily",
			"Look for a thing until you find it, and you'll not lose the labor",
			"Man fools himself. He prays for long life and he fears old age",
			"Manners maketh man",
			"Many a good face is under a ragged hat",
			"Many books do not use up words, many words do not use up thoughts",
			"Nature, time and patience are three great physicians",
			"Never answer a question when you are angry",
			"No matter how stout one beam, it cannot support a house",
			"Of a dead leopard we keep the skin, of man his reputation",
			"One dog barks at something, the rest bark at him",
			"Only he that traveled the road knows where the holes are deep",
			"Seeking fish? Don't dive in the pond, go and get a net",
			"Slow in word, swift in deed",
			"Some roads aren't meant to be traveled alone",
			"Talk does not cook rice",
			"Teaching others teaches yourself",
			"Fix your roof befores it rains, dig your well before you're thirsty",
			"The best time to plant a tree was 20 years ago, the second best time is now",
			"The carefull foot can walk alone",
			"The emperor is rich, but he cannot buy one extra year",
			"The fire you burn for your enemey often burns yourself more than him",
			"The man who strikes first admits that his ideas have given out",
			"The mind is the emperor of the body",
			"One who first resorts to violence shows he has no more arguments",
			"The palest ink is better then the best memory",
			"The pen of tongue should be dipped in the ink of the heart",
			"The remedy for dirt is soap and water, the remedy for dying is living",
			"There are two sides to every question",
			"Those who hear not the music think the dancers mad",
			"Two talkers are not worth one good listener",
			"Victory has a hundred fathers; defeat is an orphan",
			"What you cannot avoid, welcome",
			"When you say one thing, the clever person understands three",
			"When you want to test the depths of a stream, don't use both feet",
			"Whenever the water rises, the boat will rise too",
			"You can hardly make a friend in a minute but you can easily offend",
			"You can't clap with one hand",
			"You cannot propel yourself foward by patting yourself on the back",
			"You must scale the mountian, if you want to admire the view",
			"You want no one to know? Then don't do it",
			"To fall seven times and rise eight, life starts from now"
		};
	}
	
}
